<?php
namespace Socks5;
use Helper\Logger;
use Net\Event;
use Net\Connection;

require_once "Socks5Trait.php";
require_once "Socks5Connection.php";
require_once "Net/Connection.php";
require_once dirname(__DIR__) . "/Helper/Logger.php";

class Socks5Server {
    
    const LOG_FILE='socks5.access.log';
    
    use Socks5Trait;
    
    public function __construct($port = null, $address = '0.0.0.0', $listenCount = 0)
    {
        Logger::$LOG_FILE = self::LOG_FILE;
        $server = new Socks5Connection();
        $server->setFlag('server');
        $server->listening($address, $port, $listenCount);
    
        if($server->getLastError()) {
            Logger::record("Linstening to $address:$port fail: ({$server->getLastError()}) {$server->getLastErrorMessage()}");
            return;
        }
    
        $server
            ->onReady(function(Event $e) use($address, $port) {
                Logger::record('Socks5 Proxy server ready!');
                Logger::record("Listening in $address:$port");
            })
            ->onNewConnect(function(Event $e) {
                /** @var Socks5Connection $connection */
                $connection = $e->getConnection();
                $connection->setFlag('client');
                Logger::record('new connect  ' . $connection->toString());
        
                $connection
                    ->onRecv(function (Event $e) use ($connection) {
                        //协议握手完成, 直接将内容交给代理连接传输
                        if($connection->haveHandshake()) {
                            $connection->forward($e->getMessage());
                            return;
                        }
                        
                        $connection->debugPackageRcv($e->getMessage());
                        
                        //协商过身份验证方法，直接处理代理请求
                        if($connection->haveMethodSelect()) {
                            $this->_processingRequest($e);
                            return;
                        }
    
                        //交互从身份验证方法的协商开始
                        $this->_processingMethodSelect($e);
                    })
                    ->beforeClose(function() use ($connection) {
                        Logger::record('connect closed  ' . $connection);
                    })
                    ->onClose(function() use ($connection){
                        $proxyConnection = $connection->getForwardTo();
                
                        if($proxyConnection) {
                            $proxyConnection->close();
                        }
                
                        $connection->setForwardTo(null);
                    })
                ;
            })
        ;
        
        Connection::dispatch();
    }
    
    /**
     * 协议方法选择
     * @param Event $e
     */
    protected function _processingMethodSelect(Event $e) {
        $msg = $e->getMessage();
        /** @var Socks5Connection $connect */
        $connect = $e->getConnection();
        $head = $this->_parseHandshakePackage($msg);
        
        if($head['version'] != $this->VER) {
            $connect->close();
            return;
        }
        
        if(!in_array($this->METHOD_ANONYMOUS, $head['support_methods'])) {
            Logger::record(">>> No methods available", false);
            $connect->sendServerMethodSelect($this->METHOD_NONE);
            $connect->close();
            return;
        }
    
        Logger::record(">>> Select method: ANONYMOUS", false);
        $connect->sendServerMethodSelect($this->METHOD_ANONYMOUS);
        $connect->setMethodSelectd(true);
    }
    
    /**
     * 协议请求
     * @param Event $e
     */
    protected function _processingRequest(Event $e) {
        $result = $this->_parseRequestPackage($e->getMessage());
        /** @var Socks5Connection $originConnection */
        $originConnection = $e->getConnection();
        
        switch($result['address_type']) {
            //域名
            case $this->ADDRESS_TYPE_DOMAINNAME:
                if(empty($result['domain'])) {
                    $originConnection->sendServerRepRequest($this->REP_TYPE_CONNECTION_REFUSED)->close();
                    return;
                }
    
                $result['address'] = $this->_queryIp($result['domain']);
                Logger::record("$originConnection {$result['domain']} resolve to {$result['address']}");
                
                if(empty($result['address'])) {
                    $originConnection->sendServerRepRequest($this->REP_TYPE_CONNECTION_REFUSED)->close();
                    return;
                }
                
            //IP地址
            case $this->ADDRESS_TYPE_IPV4:
                //连接指定服务器请求
                if($result['cmd'] == $this->CMD_CONNECT) {
                    //没有ip或端口, 拒绝代理
                    if(empty($result['address']) || empty($result['port'])) {
                        $originConnection->sendServerRepRequest($this->REP_TYPE_CONNECTION_REFUSED)->close();
                        return;
                    }
                    
                    //建立代理连接, 用于和客户端交换数据
                    $proxyConnection = new Socks5Connection();
                    $proxyConnection->setFlag('proxy');
        
                    $proxyConnection
                        ->onConnect(function() use ($originConnection, $proxyConnection, $result) {
                            //代理连接 连接到远程目标服务器成功
                            //向客户端反馈, 老子可以开始接收数据了
                            Logger::record(">>> It is ready for interactive data", false);
                            $originConnection->sendServerRepRequest($this->REP_TYPE_SUCCESS, $result['address'], $result['port']);
                        })
                        ->onConnectFail(function (Event $e) use ($originConnection, $proxyConnection, $result) {
                            //连接目标远程服务器失败, 反馈给客户端, 这个网站老子也访问不了
                            //再见!
                            Logger::record("$proxyConnection {$result['address']}:{$result['port']} connect fail! {$e->getErrorMessage()}", false);
                            $originConnection->sendServerRepRequest($this->REP_TYPE_HOST_UNREACHABLE);
                            $originConnection->close();
                        })
                        ->onRecv(function(Event $e) use ($proxyConnection) {
                            //代理连接收到数据, 转发给客户端
                            $proxyConnection->forward($e->getMessage());
                        })

                        ->onClose(function() use($proxyConnection) {
                            $forwardToConnection = $proxyConnection->getForwardTo();
                            $proxyConnection->setForwardTo(null);
                            
                            if($forwardToConnection) {
                                $forwardToConnection->close();
                            }
                        })
                        ->beforeClose(function() use ($proxyConnection) {
                            Logger::record("connect closed {$proxyConnection} ForwardSize={$proxyConnection->getForwardSizeHumanReadable()}");
                        })
                    ;
    
                    //连接正在建立
                    //将代理连接和客户端建立互转关系
                    $proxyConnection->setForwardTo($originConnection);
                    $originConnection->setForwardTo($proxyConnection);
    
                    //已完成握手
                    $originConnection->setHandShake(true);
                    
                    Logger::record($proxyConnection . " connecting to {$result['address']}:{$result['port']}");
                    
                    //连接远程目标服务器
                    $proxyConnection->setConnectTimeout(10);
                    $proxyConnection->connect($result['address'], $result['port']);
                } else if($result['cmd'] == $this->CMD_UDP) {
                    //udp尚未实现, 拒绝代理
                    $originConnection->sendServerRepRequest($this->REP_TYPE_COMMAND_NOT_SUPPORTED)->close();
                } else {
                    //其他协议不支持
                    $originConnection->sendServerRepRequest($this->REP_TYPE_COMMAND_NOT_SUPPORTED)->close();
                }
            break;
            
            default:
                //其他类型不支持
                $originConnection->sendServerRepRequest($this->REP_TYPE_ADDRESS_TYPE_NOT_SUPPORT)->close();
        }
    }
    
    protected function _queryIp($domain) {
        return Connection::queryIp($domain);
    }
}